Somewhat like ctypes
and similar libraries in Python, Julia has a built-in ccall
feature to call functions in external compiled (C ABI) libraries.
In [1]:
ccall(:printf, Cint, (Ptr{Uint8},), "Hello, world!")
Out[1]:
The format is ccall(function name, return type, argument types, arguments...)
.
You can also call functions in arbitrary shared libraries / DLLs:
In [2]:
mysin(x) = ccall((:sin,"libm"), Cdouble, (Cdouble,), x)
Out[2]:
In [3]:
mysin(3.0) - sin(3.0)
Out[3]:
In [4]:
mysin(3) # note that Julia automatically converts types as necessary
Out[4]:
Unlike Python, however, Julia's speed means that it is perfectly fine to call C functions operating on small data, like single numbers — you don't have to "vectorize" on the C side first, and you can instead vectorize on the Julia side.
In [5]:
@vectorize_1arg Real mysin
Out[5]:
In [6]:
mysin([1,2,3,4])
Out[6]:
In [7]:
code_native(mysin, (Float64,))
Thanks to a package called PyCall
, Julia can call arbitrary Python functions by calling directly into CPython's libpython
:
In [8]:
using PyCall
In [9]:
@pyimport math as pymath
In [10]:
pymath.cos(3)
Out[10]:
Let's break this down. When you run pymath.sin(3)
, Julia:
3
into the corresponding Python object via PyObject(3)
.pymath.sin
via the libpython
routine PyObject_Call
.In terms of lower-level steps, it is doing:
In [11]:
three = PyObject(3) # calls PyInt_FromSsize_t in CPython library
Out[11]:
One slight annoyance is that Julia doesn't (yet) let you override .
, so foo.bar
in Python generally becomes foo[:bar]
in Julia (or foo["bar"]
if you want to leave the result as an unconverted Python object). This will change in a future Julia release.
In [12]:
mathmodule = pyimport("math") # calls PyImport_AddModule in CPython
sinfunc = mathmodule["sin"] # calls PyObject_GetAttrString
Out[12]:
In [13]:
returnval = pycall(sinfunc, PyObject, three) # calls PyObject_Call
Out[13]:
In [14]:
convert(Float64, returnval) # if we know the type we want, we can specify it
# calls PyFloat_AsDouble in CPython
Out[14]:
In [15]:
convert(PyAny, returnval) # if we don't know the type we want, PyAny will detect it
Out[15]:
PyCall allows large arrays and dictionaries to be passed without making a copy.
For example, Julia arrays are wrapped by NumPy arrays with shared data.
In [16]:
A = rand(3,5)
Out[16]:
In [17]:
Apy = PyObject(A)
Out[17]:
In [18]:
A[1,1] = 17
Apy
Out[18]:
In [19]:
@pyimport numpy as np
x = [-100, 39, 59, 55, 20]
np.irr(x)
Out[19]:
By default, PyCall makes a copy of arrays when converting from Python back to Julia:
In [20]:
np.cumsum(x)
Out[20]:
But we can specify a copy-free return if desired by calling the lower-level pycall
function and specifying the desired return type as PyArray
:
In [21]:
xsum = pycall(np.pymember("cumsum"), PyArray, x)
Out[21]:
The resulting NumPy array can be passed to other Julia routines without making a copy:
In [22]:
mean(xsum)
Out[22]:
There is also a PyVector
type that you can use to wrap Python lists without making a copy:
In [23]:
syspath = pyimport("sys")["path"]
Out[23]:
In [24]:
syspath_julia = PyVector(syspath)
Out[24]:
There is also a PyDict
type that you can use to share a dictionary between Julia and Python.
In [25]:
d = PyDict()
Out[25]:
In [26]:
d["hello"] = 7
d[23] = "goodbye"
d
Out[26]:
For fun, we'll use pyeval
to pass d
as a local variable dict
to an arbitrary string of Python code that we want to evaluate, in this case a list comprehension in Python:
In [27]:
pyeval("[x for x in dict.keys()]", dict=d)
Out[27]:
Arbitrary Julia functions can be passed to Python. They get converted into callable Python objects of a custom class, whose __call__
method executes the Julia code:
In [28]:
foo(x) = x + 1
pyfoo = PyObject(foo)
Out[28]:
In [29]:
pycall(pyfoo, PyAny, 17)
Out[29]:
This is extremely useful for calling functions for optimization, root-finding, etcetera, from SciPy. For example, let's solve a transcendental equation to find a root of $f(x) = \cos(x) - x$:
In [30]:
@pyimport scipy.optimize as so
function f(x)
println(" calling f($x)")
cos(x) - x
end
so.newton(f, 1.2)
Out[30]:
There is a bit of magic going on in passing Julia functions to Python. To define a new Python type from the CPython API, we create a C PyTypeObject
struct, and we need to stick a C function pointer into its tp_call
slot to give it a __call__
method.
A C function pointer is just the address of the compiled machine instructions, and since Julia has these instructions from its JIT compiler it can give you the address of the instructions using an intrinsic called cfunction
, e.g.
In [31]:
cfunction(f, Float64, (Float64,))
Out[31]:
This ability to get C function pointers from Julia functions is the key to calling any C API expecting a callback routine, not just Python. See the blog post: Passing Julia Callback Functions to C.
To get Matplotlib working in IJulia, we had to do a bit more work, similar to what IPython had to do to get its pylab
option working. For GUI windows, we had to implement the GUI event loop for the Python GUI toolkit(s) (PyQt, Gtk, wx) in Julia. For inline plots, we had to monkey-patch Matplotlib to intercept its drawing commands and queue the figure for rendering as an image to be sent to the front-end. All of this is done by the PyPlot
Julia module:
In [32]:
using PyPlot
In [33]:
x = linspace(0,2π,1000)
plot(x, sin(3x + cos(5x)), "b--")
title("a funny plot")
Out[33]:
In [34]:
y = linspace(0,2π,50)
surf(y, y, sin(y) .* cos(y)')
Out[34]:
Really, the whole Matplotlib API is available for use. It has everything you might want (in 2d, at least), if you dig long enough through the manuals:
In [35]:
clf()
xkcd()
fig = figure(figsize=(10,5))
ax = axes()
p = plot(x,sin(3x + cos(5x)))
ax[:set_xlim]([0.0,6])
annotate("A little\nsketchy",xy=[0.98,.001],arrowprops=["arrowstyle"=>"->"],xytext=[1.3,-0.3])
xticks([])
yticks([])
xlabel("TIME")
ylabel("PRODUCTIVITY")
title("An xkcd-style plot in Julia")
ax[:spines]["top"][:set_color]("none") # Remove the top axis boundary
ax[:spines]["right"][:set_color]("none") # Remove the right axis boundary